import time
import json
import os
import ntplib
import logging
from flask import Flask, request, jsonify
from datetime import datetime
from threading import Lock
from flask_apscheduler import APScheduler
from dingtalk_bot import DingTalkBot
from concurrent.futures import ThreadPoolExecutor

# ==================== 配置 ====================
DEVICE_DB_FILE = "devices.json"
LOG_LEVEL = logging.INFO  # 可改为 logging.DEBUG
PORT = 5556

# ==================== 初始化 ====================
app = Flask(__name__)
app.config['SCHEDULER_JOB_STORES'] = {'default': {'type': 'memory'}}
app.config['SCHEDULER_EXECUTORS'] = {'default': {'type': 'threadpool'}}
app.config['SCHEDULER_TIMEZONE'] = 'Asia/Shanghai'

scheduler = APScheduler()
scheduler.init_app(app)
scheduler.start()

# 日志配置
logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)

# 线程锁
device_lock = Lock()
device_db = {}

# 线程池（用于异步执行耗时操作）
executor = ThreadPoolExecutor(2)

# ==================== 时间校准模块 ====================
ntp_offset = 0  # 时间差值（NTP时间 = 系统时间 + offset）
NTP_SERVERS = [
    "ntp.aliyun.com",
    "time.windows.com",
    "time.nist.gov",
    "cn.pool.ntp.org",
    "pool.ntp.org"
]


def get_ntp_timestamp(timeout=2):
    """ 获取一次 NTP 时间 """
    for server in NTP_SERVERS:
        try:
            client = ntplib.NTPClient()
            response = client.request(server, version=3, timeout=timeout)
            logger.info(f"[NTP] 成功从 {server} 获取时间: {response.tx_time}")
            return int(response.tx_time)
        except Exception as e:
            logger.warning(f"[NTP] 服务器 {server} 不可用: {e}")
    logger.error("[NTP] 所有服务器均失败，使用本地时间替代")
    return None


def init_time_calibration():
    """ 初始化时间校准，计算初始偏移量 """
    global ntp_offset
    ntp_time = get_ntp_timestamp()
    if ntp_time is not None:
        system_time = time.time()
        ntp_offset = ntp_time - system_time
        logger.info(f"[时间校准] 初始偏移量: {ntp_offset} 秒")
    else:
        logger.warning("[时间校准] 无法获取 NTP 时间，后续将使用系统时间")


def get_calibrated_timestamp():
    """ 获取当前“模拟 NTP 时间” """
    return int(time.time() + ntp_offset)


# ==================== 工具函数 ====================

# 加载设备数据库
def load_device_db():
    global device_db
    if os.path.exists(DEVICE_DB_FILE):
        try:
            with open(DEVICE_DB_FILE, "r", encoding="utf-8") as f:
                device_db.update(json.load(f))
            logger.info("设备数据库加载完成")
        except Exception as e:
            logger.error(f"加载设备数据库失败: {e}")
    else:
        logger.info("未找到设备数据库，将新建")


# 保存设备数据库
def save_device_db():
    try:
        with device_lock:
            with open(DEVICE_DB_FILE, "w", encoding="utf-8") as f:
                json.dump(device_db, f, indent=2, ensure_ascii=False)
            logger.debug("设备数据库已保存")
    except Exception as e:
        logger.error(f"保存设备数据库失败: {e}")


# 心跳超时检测任务
@scheduler.task('interval', id='heartbeat_monitor', seconds=5)
def heartbeat_monitor():
    now = get_calibrated_timestamp()
    logger.debug("心跳检测线程开始运行")
    with device_lock:
        for mac, info in device_db.items():
            next_time = info.get("next_heartbeat")
            name = info.get("name")
            dingtalk_config = info.get("dingtalk_config")
            last_alert_sent = info.get("last_alert_sent", False)  # 新增字段
            enabled = info.get("enabled", True)  # 新增字段

            if not enabled:
                logger.info(f"设备 {mac} 未启用，跳过检测")
                continue

            if not isinstance(next_time, int):
                logger.error(f"设备 {mac} 的 next_heartbeat 不是整数")
                continue

            if now > next_time + 10:
                msg = f"[告警] 设备 {name} ({mac}) 心跳超时！\n" \
                      f"预期心跳时间: {datetime.fromtimestamp(next_time).strftime('%Y-%m-%d %H:%M:%S')}\n" \
                      f"当前服务器时间: {datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')}"

                logger.warning(msg)

                # 如果已经发送过告警，不再重复发送
                if last_alert_sent:
                    logger.info(f"[告警] 设备 {mac} 已发送过告警，跳过重复发送")
                    continue

                # 发送告警
                sent = False
                if dingtalk_config:
                    sent = send_dingtalk_alert_sync(dingtalk_config, msg)

                # 标记已发送告警
                device_db[mac]["last_alert_sent"] = True

    save_device_db()


# 同步发送钉钉消息函数
def send_dingtalk_alert_sync(config, msg):
    try:
        bot = DingTalkBot(config)
        response = bot.send_text(msg)
        if isinstance(response, dict) and response.get("errcode") == 0:
            return True
        return False
    except Exception as e:
        logger.error(f"发送钉钉消息失败: {e}")
        return False


# 校验MAC地址
def is_valid_mac(mac):
    parts = mac.split(":")
    return len(parts) == 6 and all(len(p) == 2 and p.isalnum() for p in parts)


# ==================== 定时重新校准时间 ====================
@scheduler.task('interval', id='ntp_calibration', hours=1)
def re_calibrate_time():
    """ 定时重新校准时间偏移 """
    global ntp_offset
    logger.info("[定时任务] 正在重新校准时间偏移...")
    new_ntp_time = get_ntp_timestamp()
    if new_ntp_time is not None:
        new_offset = new_ntp_time - time.time()
        logger.info(f"[时间校准] 原偏移量: {ntp_offset} 秒，新偏移量: {new_offset} 秒")
        ntp_offset = new_offset


# ==================== 接口 ====================

@app.route('/heartbeat', methods=['POST'])
def heartbeat():
    try:
        data = request.get_json(force=True)
        if not data:
            return jsonify({"error": "无效的JSON数据"}), 400

        packet_type = data.get("type")
        if packet_type not in ["register", "heartbeat", "unregister"]:
            return jsonify({"error": "包类型必须是 register / heartbeat / unregister"}), 400

        # 提取 MAC 地址（第一个非 type 的 key）
        mac = None
        for key in data.keys():
            if key != "type":
                mac = key
                break

        if not mac or not is_valid_mac(mac):
            return jsonify({"error": "无效的MAC地址或数据格式错误"}), 400

        # 获取设备数据
        value = data.get(mac, {})
        name = value.get("name")
        uptime = value.get("uptime")
        next_heartbeat_ts = value.get("next_heartbeat")
        current_time_ts = value.get("current_time")
        dingtalk_config = value.get("dingtalk")
        enabled = value.get("enabled", True)

        # 类型校验
        if not isinstance(uptime, int) or uptime < 0:
            return jsonify({"error": "uptime 必须为非负整数"}), 400
        if not isinstance(next_heartbeat_ts, int) or not isinstance(current_time_ts, int):
            return jsonify({"error": "时间戳必须为整数"}), 400

        # ========== 处理 unregister ==========
        if packet_type == "unregister":
            with device_lock:
                if mac in device_db:
                    del device_db[mac]
                    save_device_db()
                    logger.info(f"[注销] 设备 {mac} 主动注销")
                    return jsonify({"status": "success", "message": f"设备 {mac} 已注销"}), 200
                else:
                    return jsonify({"error": "设备未注册，无法注销"}), 400

        # ========== 重启检测函数（内嵌定义或可提取为全局函数）==========
        def check_and_alert_restart():
            if mac not in device_db:
                return False  # 首次注册，无法判断重启

            last_data = device_db[mac]
            last_uptime = last_data.get("uptime", 0)
            last_time_ts = last_data.get("current_time", 0)

            # 核心判断：当前运行时间小于上次，说明重启了
            if uptime < last_uptime:
                msg = f"[重启检测] 设备 {name or mac} ({mac}) 可能已重启！\n" \
                      f"上次运行时间: {last_uptime}s\n" \
                      f"当前运行时间: {uptime}s"

                logger.warning(msg)

                # 发送钉钉告警（异步）
                if dingtalk_config:
                    executor.submit(send_dingtalk_alert_sync, dingtalk_config, msg)

                # 重置告警标记
                device_db[mac]["last_alert_sent"] = False
                return True

            return False

        # ========== 加锁处理设备状态 ==========
        with device_lock:
            # 如果是心跳包，但设备未注册，拒绝
            if packet_type == "heartbeat":
                if mac not in device_db:
                    return jsonify({"error": "设备未注册，请先注册"}), 400

                # 检查是否启用
                if not device_db[mac].get("enabled", True):
                    logger.info(f"设备 {mac} 未启用，跳过心跳处理")
                    return jsonify({
                        "status": "success",
                        "message": f"设备 {mac} 未启用"
                    }), 200

            # ========== 通用：注册或心跳时都执行重启检测 ==========
            restart_detected = check_and_alert_restart()

            # ========== 根据包类型更新设备信息 ==========
            if packet_type == "register":
                # 如果是重启后注册，可选：调整 next_heartbeat
                if restart_detected:
                    heartbeat_interval = 30  # 假设心跳间隔为 30 秒
                    next_heartbeat_ts = get_calibrated_timestamp() + heartbeat_interval

                # 更新设备信息
                device_db[mac] = {
                    "name": name,
                    "uptime": uptime,
                    "next_heartbeat": next_heartbeat_ts,
                    "current_time": current_time_ts,
                    "dingtalk_config": dingtalk_config,
                    "last_alert_sent": False,
                    "enabled": enabled,
                    "last_heartbeat": get_calibrated_timestamp()
                }
                logger.info(f"[注册] MAC: {mac} {'(检测到重启)' if restart_detected else ''}")

            elif packet_type == "heartbeat":
                # 更新配置（如钉钉配置可能变化）
                if dingtalk_config:
                    device_db[mac]["dingtalk_config"] = dingtalk_config

                # 正常更新字段
                device_db[mac].update({
                    "name": name,
                    "uptime": uptime,
                    "next_heartbeat": next_heartbeat_ts,
                    "current_time": current_time_ts,
                    "last_heartbeat": get_calibrated_timestamp(),
                })

                # 可选：记录时间差异用于调试
                last_time = device_db[mac].get("current_time", current_time_ts)
                last_uptime_val = device_db[mac].get("uptime", uptime)
                time_diff = current_time_ts - last_time
                uptime_diff = uptime - last_uptime_val

                if abs(uptime_diff - time_diff) > 5:
                    logger.warning(f"[时间异常] 设备 {mac}: "
                                 f"时间差={time_diff}s, 运行时间差={uptime_diff}s")

        # ========== 保存数据库（在锁外持久化）==========
        save_device_db()

        logger.info(f"[心跳] 成功处理 {packet_type} 请求，MAC: {mac}")

        # 返回响应
        response_data = {
            "status": "success",
            "message": f"{packet_type} 来自 {mac}",
        }

        # 仅在 heartbeat 时返回差异信息
        if packet_type == "heartbeat":
            response_data["time_diff"] = time_diff
            response_data["uptime_diff"] = uptime_diff

        return jsonify(response_data), 200

    except Exception as e:
        logger.error(f"处理心跳请求出错: {e}", exc_info=True)
        return jsonify({"error": "内部服务器错误"}), 500

if __name__ == '__main__':
    # 初始化时间校准
    init_time_calibration()

    # 加载设备数据库
    load_device_db()

    # 运行Flask应用
    app.run(host='0.0.0.0', port=PORT)



